/****************************************************************************** * Copyright (C) Ultraleap, Inc. 2011-2021. * * * * Use subject to the terms of the Apache License 2.0 available at * * http://www.apache.org/licenses/LICENSE-2.0, or another agreement * * between Ultraleap and you, your company or other organization. * ******************************************************************************/ using Leap.Interaction.Internal.InteractionEngineUtility; using Leap.Unity.Attributes; using Leap.Unity.Interaction.Internal; using Leap.Unity.Query; using Leap.Unity.Space; using System; using System.Collections.Generic; using UnityEngine; namespace Leap.Unity.Interaction { public enum ContactForceMode { Object, UI }; /// /// InteractionBehaviours are components that enable GameObjects to interact with /// interaction controllers (InteractionControllerBase) in a physically intuitive way. /// /// By default, they represent objects that can be poked, prodded, smacked, grasped, /// and thrown around by Interaction controllers, including Leap hands. They also /// provide a thorough public API with settings and hovering, contact, and grasping /// callbacks for creating physical interfaces or overriding the default physical /// behavior of the object. /// /// In documentation and some method calls, GameObjects with an InteractionBehaviour /// component may be referred to as interaction objects. /// [RequireComponent(typeof(Rigidbody))] public class InteractionBehaviour : MonoBehaviour, IInteractionBehaviour { public const float MAX_ANGULAR_VELOCITY = 100F; #region Public API #region Hovering API /// Gets whether any interaction controller is nearby. public bool isHovered { get { return _hoveringControllers.Count > 0; } } /// /// Gets the closest interaction controller to this object, or null if no controller is nearby. /// Leap hands and supported VR controllers both count as "controllers" for the purposes of /// this getter. /// public InteractionController closestHoveringController { get { return _closestHoveringController; } } /// Gets the closest Leap hand to this object, or null if no hand is nearby. public Hand closestHoveringHand { get { return _closestHoveringHand == null ? null : _closestHoveringHand.leapHand; } } /// /// Gets the distance from this object to the palm of the closest hand to this object, /// or float.PositiveInfinity of no hand is nearby. /// public float closestHoveringControllerDistance { get { return _closestHoveringControllerDistance; } } /// /// Gets all of the interaction controllers hovering near this object, whether they /// are Leap hands or supported VR controllers. /// public ReadonlyHashSet hoveringControllers { get { return _hoveringControllers; } } /// /// Gets whether this object is the primary hover for any interaction controller. /// public bool isPrimaryHovered { get { return _primaryHoveringControllers.Count > 0; } } /// /// Gets the closest primary hovering interaction controller for this object, if it has one. /// An interaction controller can be a Leap hand or a supported VR controller. Any of these /// controllers can be the primary hover for this interaction object only if the controller is /// closer to it than any other interaction object. If there are multiple such controllers, /// this getter will return the closest one. /// public InteractionController primaryHoveringController { get { return _closestPrimaryHoveringController; } } /// /// Gets the set of all interaction controllers primarily hovering over this object. /// public ReadonlyHashSet primaryHoveringControllers { get { return _primaryHoveringControllers; } } /// /// Gets the primary hovering hand for this interaction object, if it has one. /// A hand is the primary hover for an interaction object only if it is closer to that object /// than any other interaction object. If there are multiple such hands, returns the hand /// closest to this object. /// public Hand primaryHoveringHand { get { return _closestPrimaryHoveringHand == null ? null : _closestPrimaryHoveringHand.leapHand; } } /// /// Gets the finger that is currently primarily hovering over this object, of the closest /// primarily hovering hand. Will return null if this object is not currently any Leap /// hand's primary hover. /// public Finger primaryHoveringFinger { get { if (!isPrimaryHovered) return null; return _closestPrimaryHoveringHand.leapHand .Fingers[_closestPrimaryHoveringHand.primaryHoveringPointIndex]; } } /// /// Gets the position of the primaryHoverPoint on the primary hovering interaction /// controller that is primarily hovering over this object. For example, if the primarily /// hovering controller is a Leap hand, this will be the position of the fingertip that /// is closest to this object. /// public Vector3 primaryHoveringControllerPoint { get { if (!isPrimaryHovered) return Vector3.zero; return primaryHoveringController.primaryHoveringPoint; } } /// /// Gets the distance to the primary hover point whose controller is primarily hovering over this /// object. For example, if the primary hovering controller is a Leap hand, this will return the /// distance to the fingertip that is closest to this object. /// /// If this object is not the primary hover of any interaction controller, returns positive infinity. /// public float primaryHoverDistance { get { if (!isPrimaryHovered) return float.PositiveInfinity; return primaryHoveringController.primaryHoverDistance; } } #region Hover Events /// /// Called when the object becomes hovered by any nearby interaction controllers. The hover activity /// radius is a setting specified by the Interaction Manager. /// /// /// If this event is to be fired on a given frame, it will be called before OnHoverStay, /// OnPerControllerHoverEnd, and OnHoverEnd, and it will be called after OnPerControllerHoverBegin. /// public Action OnHoverBegin; /// /// Called when the object stops being hovered by any nearby interaction controllers. The hover activity /// radius is a setting specified by the Interaction Manager. /// /// /// If this event is to be fired on a given frame, it will be called before OnPerControllerHoverBegin, /// OnHoverBegin, and OnHoverStay, and it will be called after OnPerControllerHoverEnd. /// public Action OnHoverEnd; /// /// Called during every fixed (physics) frame in which one or more interaction controller is /// within the hover activity radius around this object. The hover activity radius is a setting /// specified by the Interaction Manager. /// /// /// "Stay" methods are always called after their "Begin" and "End" counterparts. /// public Action OnHoverStay; /// /// Called whenever an interaction controller enters the hover activity radius around this /// interaction object. The hover activity radius is a setting specified by the Interaction Manager. /// /// /// If this event is to be fired on a given frame, it will be called after OnPerControllerHandHoverEnd /// and before OnHoverStay. /// public Action OnPerControllerHoverBegin; /// /// Called whenever an interaction controller leaves the hover activity radius around this /// interaction object. The hover activity radius is a setting specified by the Interaction Manager. /// /// /// If this event is to be fired on a given frame, it will be called before OnPerControllerHoverBegin /// and before OnHoverStay. /// public Action OnPerControllerHoverEnd; #endregion #region Primary Hover Events /// /// Called when the object becomes primarily hovered by any interaction controllers, if the object /// was not primarily hovered by any controllers on the previous frame. /// /// /// If this event is fired on a given frame, it will be called before OnPrimaryHoverStay, and it /// will be called after OnPrimaryHoverEnd. /// public Action OnPrimaryHoverBegin; /// /// Called when the object ceases being the primary hover of any interaction controllers, if the /// object was primarily hovered by one or more controllers on the previous frame. /// /// /// If this event is fired on a given frame, it will be called before OnPrimaryHoverStay and /// OnPrimaryHoverBegin. /// public Action OnPrimaryHoverEnd; /// /// Called every fixed (physics) frame in which one or more interaction controllers is primarily /// hovering over this object. Only one object may be the primary hover of a given controller at /// any one time. /// /// /// "Stay" events are fired after any "End" and "Begin" events have been fired. /// public Action OnPrimaryHoverStay; /// /// Called whenever an interaction controller (a Leap hand or supported VR controller) begins primarily /// hovering over this object. Only one interaction object can be the primary hover of a given controller /// at a time. /// /// /// If this event is to be fired on a given frame, it will be called before OnPrimaryHoverStay, /// and it will be called after OnPerControllerPrimaryHoverEnd. /// public Action OnPerControllerPrimaryHoverBegin; /// /// Called whenever an interaction controler (a Leap hand or supported VR controller) stops primarily /// hovering over this object. Only one interaction object can be the primary hover of a given controller /// at a time. /// /// /// If this event is to be fired on a given frame, it will be called before OnPerControllerPrimaryHoverBegin /// and OnPrimaryHoverStay. /// public Action OnPerControllerPrimaryHoverEnd; #endregion #endregion #region Grasping API /// Gets whether this object is grasped by any interaction controller. public bool isGrasped { get { return _graspingControllers.Count > 0; } } /// /// Gets the controller currently grasping this object. Warning: If allowMultigrasp is enabled on /// this object, it might have multiple grasping controllers, in which case this will only return one /// of the controllers grasping this object, and there is no guarantee on which controller is returned! /// If no controllers (Leap hands or supported VR controllers) are currently grasping this object, /// returns null. /// public InteractionController graspingController { get { return _graspingControllers.Query().FirstOrDefault(); } } /// /// Gets the set of all interaction controllers currently grasping this object. Interaction /// controllers include Leap hands via InteractionHand and supported VR controllers. /// public ReadonlyHashSet graspingControllers { get { return _graspingControllers; } } private HashSet _graspingHandsBuffer = new HashSet(); /// /// Gets a set of all Leap hands currently grasping this object. /// public ReadonlyHashSet graspingHands { get { _graspingHandsBuffer.Clear(); _graspingControllers.Query().OfType().FillHashSet(_graspingHandsBuffer); return _graspingHandsBuffer; } } /// /// Gets whether the object is currently suspended. An object is "suspended" if it /// is currently grasped by an untracked controller. For more details, refer to /// OnSuspensionBegin. /// public bool isSuspended { get { return _suspendingController != null; } } /// /// Nonkinematic grasping motion applies clamped velocities to Interaction Behaviours /// when they are grasped to move them to their target position and rotation in the /// grasping hand. If a controller applies its SwapGrasp method to an interaction /// object that didn't reach its target pose due to velocity clamping, the /// swapped-out object will inherit the offset as a new target pose relative to the /// hand. /// /// To prevent slippage in this scenario, we always track the latest scheduled grasp /// pose for interaction objects here, and use it whenever possible in the SwapGrasp /// method. /// public Pose? latestScheduledGraspPose = null; #region Grasp Events /// /// Called directly after this grasped object's Rigidbody has had its position and rotation set /// by its currently grasping controller(s). Subscribe to this callback if you'd like to override /// the default behaviour for grasping objects, for example, to constrain the object's position or rotation. /// /// Use InteractionBehaviour.rigidbody.position and InteractionBehaviour.rigidbody.rotation to set the /// object's position and rotation. Merely setting the object's Transform's position and rotation is not /// recommended unless you understand the difference. /// /// /// This method is called after any OnGraspBegin or OnGraspEnd callbacks, but before OnGraspStay. It is /// also valid to move the Interaction object (via its Rigidbody) in OnGraspStay, although OnGraspStay does /// not provide pre- and post-solve data in its callback signature. /// public GraspedMovementEvent OnGraspedMovement = (preSolvedPos, preSolvedRot, solvedPos, solvedRot, graspingControllers) => { }; /// /// Called when the object becomes grasped, if it was not already held by any interaction controllers on the /// previous frame. /// /// /// If this event is fired on a given frame, it will occur after OnGraspEnd and before OnGraspStay. /// public Action OnGraspBegin; /// /// Called when the object is no longer grasped by any interaction controllers. /// /// /// If this event is fired on a given frame, it will occur before OnGraspBegin and OnGraspStay. /// public Action OnGraspEnd; /// /// Called every fixed (physics) frame during which this object is grasped by one or more hands. /// /// Unless allowMultigrasp is set to true, only one hand will ever be grasping an object at any given /// time. /// /// /// If this event is fired on a given frame, it will be fired after all other grasping callbacks, including /// OnGraspedMovement. /// public Action OnGraspStay; /// /// Called whenever an interaction controller grasps this object. /// /// Unless allowMultigrasp is set to true, only one controller will ever be grasping an object at any given /// time. /// /// /// If this event is fired on a given frame, it will be called after OnPreControllerGraspEnd and before /// OnGraspStay. /// public Action OnPerControllerGraspBegin; /// /// Called whenever an interaction controller stops grasping this object. /// /// Unless allowMultigrasp is set to true, only one controller will ever be grasping an object at any given /// time. If a new controller grasps an object while allowMultigrasp is disabled, the object will first /// receive the end grasp event before receiving the begin grasp event for the newly grasping controller. /// /// /// If this event is fired on a given frame, it will be before all other grasping callbacks. /// public Action OnPerControllerGraspEnd; /// /// Called when the interaction controller that is grasping this interaction object loses tracking. This can /// occur if the controller is occluded from the sensor that is tracking it, e.g. by as the user's body or /// an object in the real world. /// /// An object is "suspended" if it is currently grasped by an untracked controller. /// /// By default, suspended objects will hang in the air until the interaction controller grasping them /// resumes tracking. Subscribe to this callback and OnResume to implement, e.g., the object disappearing /// and re-appearing. /// public Action OnSuspensionBegin; /// /// Called when an object ceases being suspended. An object is suspended if it is currently grasped by /// an untracked controller. /// /// Grasping a suspended object with a different controller will cease suspension of the object, and will /// invoke OnSuspensionEnd, although the input to OnSuspensionEnd will be the newly grasping controller, not /// the controller that suspended the object. OnGraspEnd will also be called for the interaction controller /// that was formerly causing suspension. /// public Action OnSuspensionEnd; #endregion /// /// Releases this object from the interaction controller currently grasping it, if it /// is grasped, and returns true. If the object was not grasped, this method returns /// false. Directly after calling this method, the object is guaranteed not to be /// held. However, a grasp may retrigger on the next frame, if the Interaction /// Controller determines that the released object should be grasped. The safest way /// to ensure an object is released and ungraspable is to use the interaction /// object's ignoreGrasp property. /// public bool ReleaseFromGrasp() { if (isGrasped) { InteractionController.ReleaseGrasps(this, graspingControllers); return true; } return false; } /// /// Returns (approximately) where the argument hand is grasping this object. /// If the interaction controller is not currently grasping this object, returns /// Vector3.zero, and logs an error to the Unity console. /// public Vector3 GetGraspPoint(InteractionController intController) { if (intController.graspedObject == this as IInteractionBehaviour) { return intController.GetGraspPoint(); } else { Debug.LogError("Cannot get this object's grasp point: It is not currently grasped " + "by the provided interaction controller.", intController); return Vector3.zero; } } #endregion #region Contact API /// /// Gets a set of all InteractionControllers currently contacting this interaction /// object. /// public ReadonlyHashSet contactingControllers { get { return _contactingControllers; } } /// /// Called when this object begins colliding with any interaction controllers, if the /// object was not colliding with any interaction controllers last frame. /// public Action OnContactBegin; /// /// Called when the object ceases colliding with any interaction controllers, if the /// object was colliding with interaction controllers last frame. /// public Action OnContactEnd; /// /// Called every frame during which one or more interaction controllers is colliding /// with this object. /// public Action OnContactStay; /// /// Called whenever an interaction controller begins colliding with this object. /// public Action OnPerControllerContactBegin; /// /// Called whenever an interaction controller stops colliding with this object. /// public Action OnPerControllerContactEnd; #endregion #region Forces API /// /// Adds a linear acceleration to the center of mass of this object. /// Use this instead of Rigidbody.AddForce() to accelerate an Interaction object. /// /// /// Rigidbody.AddForce() will work in most scenarios, but will produce unexpected /// behavior when interaction controllers are embedded inside an object due to soft /// contact. Calling this method instead solves that problem. /// public void AddLinearAcceleration(Vector3 acceleration) { _appliedForces = true; _accumulatedLinearAcceleration += acceleration; } /// /// Adds an angular acceleration to the center of mass of this object. /// Use this instead of Rigidbody.AddTorque() to add angular acceleration /// to an Interaction object. /// /// /// Rigidbody.AddTorque() will work in most scenarios, but will produce unexpected /// behavior when interaction controllers are embedded inside an object due to soft /// contact. Calling this method instead solves that problem. /// public void AddAngularAcceleration(Vector3 acceleration) { _appliedForces = true; _accumulatedAngularAcceleration += acceleration; } #endregion #region General API /// Use this if you want to modify the isKinematic state of an /// interaction object while it is grasped; otherwise the object's grasp /// settings may return the Rigidbody to the kinematic state of the object /// from right before it was grasped. public void SetKinematicWithoutGrasp(bool isKinematic) { if (this.isGrasped) { _wasKinematicBeforeGrasp = isKinematic; } else { _rigidbody.isKinematic = isKinematic; } } /// Use this to retrieve the isKinematic state of the interactino /// object ignoring any temporary modification to isKinematic that may be /// due to the object being grasped. public bool GetKinematicWithoutGrasp() { if (this.isGrasped) { return _wasKinematicBeforeGrasp; } else { return _rigidbody.isKinematic; } } #endregion #endregion #region Inspector [Tooltip("The Interaction Manager responsible for this interaction object.")] [SerializeField] private InteractionManager _manager; public InteractionManager manager { get { return _manager; } set { if (Application.isPlaying) { if (_manager != null && _manager.IsBehaviourRegistered(this)) { _manager.UnregisterInteractionBehaviour(this); } } _manager = value; if (Application.isPlaying) { if (_manager != null && !manager.IsBehaviourRegistered(this)) { _manager.RegisterInteractionBehaviour(this); } } } } private Rigidbody _rigidbody; #if UNITY_EDITOR new #endif /// The Rigidbody associated with this interaction object. public Rigidbody rigidbody { get { return _rigidbody; } protected set { _rigidbody = value; } } public ISpaceComponent space { get; protected set; } [Header("Interaction Overrides")] [Tooltip("This object will not receive hover callbacks from left controllers, right " + "controllers, or either hand if this mode is set to anything other than " + "None.")] [SerializeField] private IgnoreHoverMode _ignoreHoverMode = IgnoreHoverMode.None; public IgnoreHoverMode ignoreHoverMode { get { return _ignoreHoverMode; } set { _ignoreHoverMode = value; if (_ignoreHoverMode != IgnoreHoverMode.None) { ClearHoverTracking(onlyInvalidControllers: true); } } } [SerializeField, HideInInspector] private bool _isIgnoringAllHoverState = false; [Tooltip("Interaction controllers will not be able to mark this object as their " + "primary hover if this property is checked. Primary hover requires hovering " + "enabled to function, but it can be disabled independently of hovering.")] [SerializeField] [DisableIf("_isIgnoringAllHoverState", isEqualTo: true)] private bool _ignorePrimaryHover = false; public bool ignorePrimaryHover { get { return _ignorePrimaryHover; } set { _ignorePrimaryHover = value; if (_ignorePrimaryHover) { ClearPrimaryHoverTracking(); } } } [Tooltip("Interaction controllers will not be able to touch this object if this " + "property is checked.")] [SerializeField] private bool _ignoreContact = false; public bool ignoreContact { get { return _ignoreContact; } set { _ignoreContact = value; if (_ignoreContact) { ClearContactTracking(); } } } [Tooltip("This object will not receive grasping callbacks from left controllers, right " + "controllers, or either hand if this mode is set to anything other than " + "None.")] [SerializeField] private IgnoreHoverMode _ignoreGraspingMode = IgnoreHoverMode.None; public IgnoreHoverMode ignoreGraspingMode { get { return _ignoreGraspingMode; } set { _ignoreGraspingMode = value; if (isGrasped && _ignoreGraspingMode != IgnoreHoverMode.None) { if ((_ignoreGraspingMode == IgnoreHoverMode.Left && graspingControllers.Query().FirstOrNone(x => x.isLeft) != null) || (_ignoreGraspingMode == IgnoreHoverMode.Right && graspingControllers.Query().FirstOrNone(x => x.isRight) != null)) { graspingController.ReleaseGrasp(); } } } } [Tooltip("Interaction controllers will not be able to grasp this object if this " + "property is checked.")] [SerializeField] private bool _ignoreGrasping = false; public bool ignoreGrasping { get { return _ignoreGrasping; } set { _ignoreGrasping = value; if (_ignoreGrasping && isGrasped) { graspingController.ReleaseGrasp(); } } } [Header("Contact Settings")] [Tooltip("Determines how much force an interaction controller should apply to this " + "object. For interface-style objects like buttons and sliders, choose UI. " + "This will make the objects to feel lighter and more reactive to gentle " + "touches; for normal physical objects, you'll almost always want Object.")] [SerializeField] private ContactForceMode _contactForceMode = ContactForceMode.Object; public ContactForceMode contactForceMode { get { return _contactForceMode; } set { _contactForceMode = value; } } [Header("Grasp Settings")] [Tooltip("Can this object be grasped simultaneously with two or more interaction " + "controllers?")] [SerializeField] private bool _allowMultiGrasp = false; public bool allowMultiGrasp { get { return _allowMultiGrasp; } set { _allowMultiGrasp = value; } } [Tooltip("Should interaction controllers move this object when it is grasped? " + "Without this property checked, objects will still receive grasp callbacks, " + "but you will need to move them manually via script.")] [SerializeField] [OnEditorChange("moveObjectWhenGrasped")] private bool _moveObjectWhenGrasped = true; public bool moveObjectWhenGrasped { get { return _moveObjectWhenGrasped; } set { if (_moveObjectWhenGrasped != value && value == false) { if (graspedPoseHandler != null) { graspedPoseHandler.ClearControllers(); } } _moveObjectWhenGrasped = value; } } public enum GraspedMovementType { Inherit, Kinematic, Nonkinematic } [Tooltip("When the object is held by an interaction controller, how should it move to " + "its new position? Nonkinematic bodies will collide with other Rigidbodies, " + "so they might not reach the target position. Kinematic rigidbodies will " + "always move to the target position, ignoring collisions. Inherit will " + "simply use the isKinematic state of the Rigidbody from before it was " + "grasped.")] [DisableIf("_moveObjectWhenGrasped", isEqualTo: false)] public GraspedMovementType graspedMovementType; [Header("Layer Overrides")] [SerializeField] [OnEditorChange("overrideInteractionLayer")] [Tooltip("If set to true, this interaction object will override the Interaction " + "Manager's layer setting for its default layer. The interaction layer is " + "used for an object when it is not grasped and not ignoring contact.")] private bool _overrideInteractionLayer = false; public bool overrideInteractionLayer { get { return _overrideInteractionLayer; } set { _overrideInteractionLayer = value; } } [Tooltip("Sets the override layer to use for this object when it is not grasped and " + "not ignoring contact.")] [SerializeField] private SingleLayer _interactionLayer; public SingleLayer interactionLayer { get { return _interactionLayer; } protected set { _interactionLayer = value; } } [SerializeField] [OnEditorChange("overrideNoContactLayer")] [Tooltip("If set to true, this interaction object will override the Interaction " + "Manager's layer setting for its default no-contact layer. The no-contact " + "layer should not collide with the contact bone layer; it is used when the " + "interaction object is grasped or when it is ignoring contact.")] private bool _overrideNoContactLayer = false; public bool overrideNoContactLayer { get { return _overrideNoContactLayer; } set { _overrideNoContactLayer = value; } } [Tooltip("Overrides the layer this interaction object should be on when it is grasped " + "or ignoring contact. This layer should not collide with the contact bone " + "layer -- the layer interaction controllers' colliders are on.")] [SerializeField] private SingleLayer _noContactLayer; public SingleLayer noContactLayer { get { return _noContactLayer; } protected set { _noContactLayer = value; } } #endregion #region Unity Callbacks protected virtual void OnValidate() { rigidbody = GetComponent(); _isIgnoringAllHoverState = ignoreHoverMode == IgnoreHoverMode.Both; if (_isIgnoringAllHoverState) _ignorePrimaryHover = true; } protected virtual void Awake() { InitUnityEvents(); rigidbody = GetComponent(); rigidbody.maxAngularVelocity = MAX_ANGULAR_VELOCITY; } protected virtual void OnEnable() { if (manager == null) { manager = InteractionManager.instance; if (manager == null) { Debug.LogError("Interaction Behaviours require an Interaction Manager. Please " + "ensure you have an InteractionManager in your scene."); this.enabled = false; } } if (manager != null && !manager.IsBehaviourRegistered(this)) { manager.RegisterInteractionBehaviour(this); } // Make sure we have a list of all of this object's colliders. RefreshInteractionColliders(); // Refresh curved space. Currently a maximum of one (1) LeapSpace is supported per // InteractionBehaviour. foreach (var collider in _interactionColliders) { var leapSpace = collider.transform.GetComponentInParent(); if (leapSpace != null) { space = leapSpace; break; } } // Ensure physics layers are set up properly. initLayers(); } protected virtual void OnDisable() { // Remove this object's layer tracking from the manager. finalizeLayers(); if (manager != null && manager.IsBehaviourRegistered(this)) { manager.UnregisterInteractionBehaviour(this); } } protected virtual void Start() { // Check any Joint attachments to automatically be able to choose Kabsch pivot // setting (grasping). RefreshPositionLockedState(); } #endregion /// /// The InteractionManager manually calls method this after all /// InteractionControllerBase objects are updated via the InteractionManager's /// FixedUpdate(). /// public void FixedUpdateObject() { fixedUpdateLayers(); if (_appliedForces) { FixedUpdateForces(); } } #region Hovering private HashSet _hoveringControllers = new HashSet(); private InteractionController _closestHoveringController = null; private float _closestHoveringControllerDistance = float.PositiveInfinity; private InteractionHand _closestHoveringHand = null; /// /// Returns a comparative distance to this interaction object. Calculated by finding /// the smallest distance to each of the object's colliders. /// /// Any MeshColliders, however, will not have their distances calculated precisely; /// the squared distance to their bounding box is calculated instead. It is possible /// to use a custom set of colliders against which to test primary hover calculations: /// see primaryHoverColliders. /// public virtual float GetHoverDistance(Vector3 worldPosition) { float closestComparativeColliderDistance = float.PositiveInfinity; bool hasColliders = false; float testDistance = float.PositiveInfinity; if (rigidbody == null) { // The Interaction Object is probably being destroyed, or is otherwise in an // invalid state. return float.PositiveInfinity; } foreach (var collider in _interactionColliders) { if (!hasColliders) hasColliders = true; if (collider is MeshCollider) { // Native, faster ClosestPoint, but no support for off-center colliders; use to // support MeshColliders. testDistance = (Physics.ClosestPoint(worldPosition, collider, collider.attachedRigidbody.position, collider.attachedRigidbody.rotation) - worldPosition).magnitude; } // Custom, slower ClosestPoint else { // Note: Should be using rigidbody position instead of transform; this will // cause problems when colliders are moving very fast (one-frame delay). testDistance = (collider.transform.TransformPoint( collider.ClosestPointOnSurface( collider.transform.InverseTransformPoint(worldPosition))) - worldPosition).magnitude; } if (testDistance < closestComparativeColliderDistance) { closestComparativeColliderDistance = testDistance; } } if (!hasColliders) { return (this.rigidbody.position - worldPosition).magnitude; } else { return closestComparativeColliderDistance; } } public void BeginHover(List controllers) { foreach (var controller in controllers) { _hoveringControllers.Add(controller); } refreshClosestHoveringController(); foreach (var controller in controllers) { OnPerControllerHoverBegin(controller); } if (_hoveringControllers.Count == controllers.Count) { OnHoverBegin(); } } public void EndHover(List controllers) { foreach (var controller in controllers) { _hoveringControllers.Remove(controller); } refreshClosestHoveringController(); foreach (var controller in controllers) { OnPerControllerHoverEnd(controller); } if (_hoveringControllers.Count == 0) { OnHoverEnd(); } } public void StayHovered(List controllers) { refreshClosestHoveringController(); OnHoverStay(); } private void refreshClosestHoveringController() { float closestControllerDistance = float.PositiveInfinity; _closestHoveringController = getClosestController(_hoveringControllers, out closestControllerDistance); _closestHoveringControllerDistance = closestControllerDistance; float closestHandDistance = float.PositiveInfinity; _closestHoveringHand = getClosestController(_hoveringControllers, out closestHandDistance, controller => controller.intHand != null) as InteractionHand; // closestHandDistance unused for now. } private InteractionController getClosestController(HashSet controllers, out float closestDistance, Func filter = null) { InteractionController closestHoveringController = null; float closestHoveringControllerDist = float.PositiveInfinity; foreach (var controller in controllers) { if (filter != null && filter(controller) == false) continue; float distance = GetHoverDistance(controller.hoverPoint); if (closestHoveringHand == null || distance < closestHoveringControllerDist) { closestHoveringController = controller; closestHoveringControllerDist = distance; } } closestDistance = closestHoveringControllerDist; return closestHoveringController; } /// /// Clears hover tracking state for this object on all of the currently-hovering /// controllers. New hover state will begin anew on the next fixed frame if the /// appropriate conditions for hover are still fulfilled. /// /// Optionally, only clear hover tracking state for controllers that should be /// ignoring hover for this interaction object due to its ignoreHoverMode. /// public void ClearHoverTracking(bool onlyInvalidControllers = false) { var tempControllers = Pool>.Spawn(); try { foreach (var controller in hoveringControllers) { if (onlyInvalidControllers && this.ShouldIgnoreHover(controller)) { tempControllers.Add(controller); } } foreach (var controller in tempControllers) { controller.ClearHoverTrackingForObject(this); } } finally { tempControllers.Clear(); Pool>.Recycle(tempControllers); } } private HashSet _primaryHoveringControllers = new HashSet(); private InteractionController _closestPrimaryHoveringController = null; private InteractionHand _closestPrimaryHoveringHand = null; public void BeginPrimaryHover(List controllers) { foreach (var controller in controllers) { _primaryHoveringControllers.Add(controller); } refreshClosestPrimaryHoveringController(); foreach (var controller in controllers) { OnPerControllerPrimaryHoverBegin(controller); } if (_primaryHoveringControllers.Count == controllers.Count) { OnPrimaryHoverBegin(); } } public void EndPrimaryHover(List controllers) { foreach (var controller in controllers) { _primaryHoveringControllers.Remove(controller); } refreshClosestPrimaryHoveringController(); foreach (var controller in controllers) { OnPerControllerPrimaryHoverEnd(controller); } if (_primaryHoveringControllers.Count == 0) { OnPrimaryHoverEnd(); } } public void StayPrimaryHovered(List controllers) { refreshClosestPrimaryHoveringController(); OnPrimaryHoverStay(); } private void refreshClosestPrimaryHoveringController() { _closestPrimaryHoveringController = getClosestPrimaryHoveringController(); _closestPrimaryHoveringHand = getClosestPrimaryHoveringController((controller) => controller.intHand != null) as InteractionHand; } private InteractionController getClosestPrimaryHoveringController(Func filter = null) { InteractionController closestController = null; float closestDist = float.PositiveInfinity; foreach (var controller in _primaryHoveringControllers) { if (filter != null && filter(controller) == false) continue; if (closestController == null || controller.primaryHoverDistance < closestDist) { closestController = controller; closestDist = controller.primaryHoverDistance; } } return closestController; } /// /// Clears primary hover tracking state for this object on all of the currently- /// primary-hovering controllers. New priamry hover state will begin anew on the next /// fixed frame if the appropriate conditions for primary hover are still fulfilled. /// public void ClearPrimaryHoverTracking() { var tempControllers = Pool>.Spawn(); try { foreach (var controller in primaryHoveringControllers) { tempControllers.Add(controller); } foreach (var controller in tempControllers) { controller.ClearPrimaryHoverTracking(); } } finally { tempControllers.Clear(); Pool>.Recycle(tempControllers); } } /// /// Gets the List of Colliders used for hover distance checking for this Interaction /// object. Hover distancing checking will affect which object is chosen for an /// interaction controller's primary hover, as well as for determining this object's /// closest hovering controller. /// /// RefreshInteractionColliders() will automatically populate the colliders List with /// the this rigidbody's colliders, but is only called once on Start(). If you change /// the colliders for this object at runtime, you should call RefreshInteractionColliders() /// to keep the _hoverColliders list up-to-date. /// /// /// If you're feeling brave, you can manually modify this list yourself. /// /// Hover candidacy is determined by a hand-centric PhysX sphere-check against the /// Interaction object's rigidbody's attached colliders. This behavior cannot be /// changed, even if you modify the contents of primaryHoverColliders. /// /// However, primary hover is determined by performing distance checks against the /// colliders in the primaryHoverColliders list, so it IS possible to use different /// collider(s) for primary hover checks than are used for hover candidacy, by /// modifying the collider contents of this list. This will also affect which hand is /// chosen by this object as its closestHoveringHand. /// public List primaryHoverColliders { get { return _interactionColliders; } } #endregion #region Contact private HashSet _contactingControllers = new HashSet(); public void BeginContact(List controllers) { foreach (var controller in controllers) { _contactingControllers.Add(controller); OnPerControllerContactBegin(controller); } if (_contactingControllers.Count == controllers.Count) { OnContactBegin(); } } public void EndContact(List controllers) { foreach (var controller in controllers) { _contactingControllers.Remove(controller); OnPerControllerContactEnd(controller); } if (_contactingControllers.Count == 0) { OnContactEnd(); } } public void StayContacted(List controllers) { OnContactStay(); } /// /// Clears contact tracking for this object on any currently-contacting controllers. /// If the object is still contacting controllers and they are appropriately enabled, /// contact will begin anew on the next fixed frame. /// public void ClearContactTracking() { var tempControllers = Pool>.Spawn(); try { foreach (var controller in contactingControllers) { tempControllers.Add(controller); } foreach (var controller in tempControllers) { controller.ClearContactTrackingForObject(this); } } finally { tempControllers.Clear(); Pool>.Recycle(tempControllers); } } #endregion #region Grasping private HashSet _graspingControllers = new HashSet(); private bool _wasKinematicBeforeGrasp; private bool _justGrasped = false; private float _dragBeforeGrasp = 0F; private float _angularDragBeforeGrasp = 0.05F; private IGraspedPoseHandler _graspedPoseHandler; /// Gets or sets the grasped pose handler for this Interaction object. public IGraspedPoseHandler graspedPoseHandler { get { if (_graspedPoseHandler == null) { _graspedPoseHandler = new KabschGraspedPose(this); } return _graspedPoseHandler; } set { _graspedPoseHandler = value; } } private KinematicGraspedMovement _lazyKinematicGraspedMovement; private KinematicGraspedMovement _kinematicGraspedMovement { get { if (_lazyKinematicGraspedMovement == null) { _lazyKinematicGraspedMovement = new KinematicGraspedMovement(); } return _lazyKinematicGraspedMovement; } } private NonKinematicGraspedMovement _lazyNonKinematicGraspedMovement; private NonKinematicGraspedMovement _nonKinematicGraspedMovement { get { if (_lazyNonKinematicGraspedMovement == null) { _lazyNonKinematicGraspedMovement = new NonKinematicGraspedMovement(); } return _lazyNonKinematicGraspedMovement; } } private IThrowHandler _throwHandler; /// Gets or sets the throw handler for this Interaction object. public IThrowHandler throwHandler { get { if (_throwHandler == null) { _throwHandler = new SlidingWindowThrow(); } return _throwHandler; } set { _throwHandler = value; } } public void BeginGrasp(List controllers) { _justGrasped = true; // End suspension by ending the grasp on the suspending hand, // calling EndGrasp immediately. if (isSuspended) { _suspendingController.ReleaseGrasp(); } // If multi-grasp is not allowed, release the old grasp. if (!allowMultiGrasp && isGrasped) { _graspingControllers.Query().First().ReleaseGrasp(); } // Add each newly grasping hand to internal reference and pose solver. foreach (var controller in controllers) { _graspingControllers.Add(controller); if (moveObjectWhenGrasped) { graspedPoseHandler.AddController(controller); } // Fire interaction callback. OnPerControllerGraspBegin(controller); } // If object wasn't grasped before, store rigidbody settings and // fire object interaction callback. if (_graspingControllers.Count == controllers.Count) { // Remember drag settings pre-grasp, to be restored on release. _dragBeforeGrasp = rigidbody.drag; _angularDragBeforeGrasp = rigidbody.angularDrag; // Remember kinematic state. _wasKinematicBeforeGrasp = rigidbody.isKinematic; switch (graspedMovementType) { case GraspedMovementType.Inherit: break; // no change case GraspedMovementType.Kinematic: rigidbody.isKinematic = true; break; case GraspedMovementType.Nonkinematic: rigidbody.isKinematic = false; break; } // Set rigidbody drag/angular drag to zero. rigidbody.drag = 0F; rigidbody.angularDrag = 0F; OnGraspBegin(); } } public void EndGrasp(List controllers) { if (_graspingControllers.Count == controllers.Count && isSuspended) { // No grasped hands: Should not be suspended any more; // having been suspended also means we were only grasped by one hand EndSuspension(controllers[0]); } foreach (var controller in controllers) { _graspingControllers.Remove(controller); // Fire interaction callback. OnPerControllerGraspEnd(controller); if (moveObjectWhenGrasped && manager.multiGraspHoldingMode == InteractionManager.MultiGraspHoldingMode.PreservePosePerController) { // Remove each hand from the pose solver. graspedPoseHandler.RemoveController(controller); } } // Possibly re-initialize the graspedPoseHandler. if (moveObjectWhenGrasped && manager.multiGraspHoldingMode == InteractionManager.MultiGraspHoldingMode.ReinitializeOnAnyRelease) { graspedPoseHandler.ClearControllers(); foreach (var item in _graspingControllers) { graspedPoseHandler.AddController(item); } } // If the object is no longer grasped by any hands, restore state and // activate throw handler. if (_graspingControllers.Count == 0) { // Restore drag settings from prior to the grasp. rigidbody.drag = _dragBeforeGrasp; rigidbody.angularDrag = _angularDragBeforeGrasp; // Revert kinematic state. rigidbody.isKinematic = _wasKinematicBeforeGrasp; if (controllers.Count == 1) { throwHandler.OnThrow(this, controllers.Query().First()); } OnGraspEnd(); if (_justGrasped) _justGrasped = false; } } public void StayGrasped(List controllers) { if (moveObjectWhenGrasped) { Vector3 origPosition = rigidbody.position; Quaternion origRotation = rigidbody.rotation; Vector3 newPosition; Quaternion newRotation; graspedPoseHandler.GetGraspedPosition(out newPosition, out newRotation); fixedUpdateGraspedMovement(new Pose(origPosition, origRotation), new Pose(newPosition, newRotation), controllers); throwHandler.OnHold(this, controllers); } OnGraspStay(); _justGrasped = false; } protected virtual void fixedUpdateGraspedMovement(Pose origPose, Pose newPose, List controllers) { IGraspedMovementHandler graspedMovementHandler = rigidbody.isKinematic ? (IGraspedMovementHandler)_kinematicGraspedMovement : (IGraspedMovementHandler)_nonKinematicGraspedMovement; graspedMovementHandler.MoveTo(newPose.position, newPose.rotation, this, _justGrasped); OnGraspedMovement(origPose.position, origPose.rotation, newPose.position, newPose.rotation, controllers); } protected InteractionController _suspendingController = null; public void BeginSuspension(InteractionController controller) { _suspendingController = controller; OnSuspensionBegin(controller); } public void EndSuspension(InteractionController controller) { _suspendingController = null; OnSuspensionEnd(controller); } #endregion #region Forces private bool _appliedForces = false; protected Vector3 _accumulatedLinearAcceleration = Vector3.zero; protected Vector3 _accumulatedAngularAcceleration = Vector3.zero; public void FixedUpdateForces() { if (!isGrasped) { //Only apply if non-zero to prevent waking up the body if (_accumulatedLinearAcceleration != Vector3.zero) { rigidbody.velocity += _accumulatedLinearAcceleration * Time.fixedDeltaTime; } if (_accumulatedAngularAcceleration != Vector3.zero) { rigidbody.angularVelocity += _accumulatedAngularAcceleration * Time.fixedDeltaTime; } //Reset so we can accumulate for the next frame _accumulatedLinearAcceleration = Vector3.zero; _accumulatedAngularAcceleration = Vector3.zero; _appliedForces = false; } } #endregion #region Colliders protected List _interactionColliders = new List(); /// /// Recursively searches the hierarchy of this Interaction object to /// find all of the Colliders that are attached to its Rigidbody. These will /// be the colliders used to calculate distance from the controller to determine /// which object will become the primary hover. /// /// Call this method manually if you change an Interaction object's colliders /// after its Start() method has been called! (Called automatically in OnEnable.) /// public void RefreshInteractionColliders() { Utils.FindColliders(this.gameObject, _interactionColliders, includeInactiveObjects: false); _interactionColliders.RemoveAll( c => c.GetComponent() != null); // Since the interaction colliders might have changed, or appeared for the first // time, set their layers appropriately. refreshInteractionColliderLayers(); } #endregion #region Interaction Layers private int _lastInteractionLayer = -1; private int _lastNoContactLayer = -1; private void initLayers() { refreshInteractionLayer(); refreshNoContactLayer(); (manager as IInternalInteractionManager).NotifyIntObjAddedInteractionLayer(this, interactionLayer, false); (manager as IInternalInteractionManager).NotifyIntObjAddedNoContactLayer(this, noContactLayer, false); (manager as IInternalInteractionManager).RefreshLayersNow(); _lastInteractionLayer = interactionLayer; _lastNoContactLayer = noContactLayer; } private void refreshInteractionLayer() { interactionLayer = overrideInteractionLayer ? this.interactionLayer : manager.interactionLayer; } private void refreshNoContactLayer() { noContactLayer = overrideNoContactLayer ? this.noContactLayer : manager.interactionNoContactLayer; } private void fixedUpdateLayers() { using (new ProfilerSample("Interaction Behaviour: fixedUpdateLayers")) { int layer; refreshInteractionLayer(); refreshNoContactLayer(); // Update the object's layer based on interaction state. if (ignoreContact) { layer = noContactLayer; } else { if (isGrasped) { layer = noContactLayer; } else { layer = interactionLayer; } } if (this.gameObject.layer != layer) { this.gameObject.layer = layer; refreshInteractionColliderLayers(); } // Update the manager if necessary. if (interactionLayer != _lastInteractionLayer) { (manager as IInternalInteractionManager).NotifyIntObjHasNewInteractionLayer(this, oldInteractionLayer: _lastInteractionLayer, newInteractionLayer: interactionLayer); _lastInteractionLayer = interactionLayer; } if (noContactLayer != _lastNoContactLayer) { (manager as IInternalInteractionManager).NotifyIntObjHasNewNoContactLayer(this, oldNoContactLayer: _lastNoContactLayer, newNoContactLayer: noContactLayer); _lastNoContactLayer = noContactLayer; } } } private void finalizeLayers() { (manager as IInternalInteractionManager).NotifyIntObjRemovedInteractionLayer(this, interactionLayer, false); (manager as IInternalInteractionManager).NotifyIntObjRemovedNoContactLayer(this, noContactLayer, false); (manager as IInternalInteractionManager).RefreshLayersNow(); } /// /// Sets the layer state of the _interactionColliders to match the root interaction /// object if their layer differs from it. /// /// This method does NOT modify the interaction object's own layer (unless the /// interaction object has a collider on itself; which would result in a no-op). /// /// This needs to be called if the layer of the interaction object changes or if the /// object gains new colliders. /// private void refreshInteractionColliderLayers() { for (int i = 0; i < _interactionColliders.Count; i++) { if (_interactionColliders[i].gameObject.layer != this.gameObject.layer) { _interactionColliders[i].gameObject.layer = this.gameObject.layer; } } } #endregion #region Locked Position (Joint) Checking private bool _isPositionLocked = false; /// /// Returns whether the InteractionBehaviour has its position fully locked /// by its Rigidbody settings or by any attached PhysX Joints. /// /// This is useful for the GraspedMovementController to determine whether /// it should attempt to move the interaction object or merely rotate it. /// /// If the state of the underlying Rigidbody or Joints changes what this value /// should be, it will not automatically update (as an optimization) at runtime; /// instead, manually call RefreshPositionLockedState(). This is because the /// type-checks required are relatively expensive and mustn't occur every frame. /// public bool isPositionLocked { get { return _isPositionLocked; } } /// /// Call this method if the InteractionBehaviour's Rigidbody becomes or unbecomes /// fully positionally locked (X, Y, Z) or if a Joint attached to the Rigidbody /// no longer locks its position (e.g. by being destroyed or disabled). /// public void RefreshPositionLockedState() { if ((rigidbody.constraints & RigidbodyConstraints.FreezePositionX) > 0 && (rigidbody.constraints & RigidbodyConstraints.FreezePositionY) > 0 && (rigidbody.constraints & RigidbodyConstraints.FreezePositionZ) > 0) { _isPositionLocked = true; return; } else { _isPositionLocked = false; Joint[] joints = rigidbody.GetComponents(); foreach (var joint in joints) { if (joint.connectedBody == null || joint.connectedBody.isKinematic) { if (joint is FixedJoint) { _isPositionLocked = true; return; } if (joint is HingeJoint) { _isPositionLocked = true; return; } // if (joint is SpringJoint) { // no check required; spring joints never fully lock position. // } if (joint is CharacterJoint) { _isPositionLocked = true; return; } ConfigurableJoint configJoint = joint as ConfigurableJoint; if (configJoint != null && (configJoint.xMotion == ConfigurableJointMotion.Locked || (configJoint.xMotion == ConfigurableJointMotion.Limited && configJoint.linearLimit.limit == 0F)) && (configJoint.yMotion == ConfigurableJointMotion.Locked || (configJoint.yMotion == ConfigurableJointMotion.Limited && configJoint.linearLimit.limit == 0F)) && (configJoint.zMotion == ConfigurableJointMotion.Locked || (configJoint.zMotion == ConfigurableJointMotion.Limited && configJoint.linearLimit.limit == 0F))) { _isPositionLocked = true; return; } } } } } #endregion #region Unity Events [SerializeField] private EnumEventTable _eventTable; public enum EventType { HoverBegin = 100, HoverEnd = 101, HoverStay = 102, PerControllerHoverBegin = 110, PerControllerHoverEnd = 111, PrimaryHoverBegin = 120, PrimaryHoverEnd = 121, PrimaryHoverStay = 122, PerControllerPrimaryHoverBegin = 130, PerControllerPrimaryHoverEnd = 132, GraspBegin = 140, GraspEnd = 141, GraspStay = 142, PerControllerGraspBegin = 150, PerControllerGraspEnd = 152, SuspensionBegin = 160, SuspensionEnd = 161, ContactBegin = 170, ContactEnd = 171, ContactStay = 172, PerControllerContactBegin = 180, PerControllerContactEnd = 181 } private void InitUnityEvents() { // If the interaction component is added at runtime, _eventTable won't have been // constructed yet. if (_eventTable == null) _eventTable = new EnumEventTable(); setupCallback(ref OnHoverBegin, EventType.HoverBegin); setupCallback(ref OnHoverEnd, EventType.HoverEnd); setupCallback(ref OnHoverStay, EventType.HoverStay); setupCallback(ref OnPerControllerHoverBegin, EventType.PerControllerHoverBegin); setupCallback(ref OnPerControllerHoverEnd, EventType.PerControllerHoverEnd); setupCallback(ref OnPrimaryHoverBegin, EventType.PrimaryHoverBegin); setupCallback(ref OnPrimaryHoverEnd, EventType.PrimaryHoverEnd); setupCallback(ref OnPrimaryHoverStay, EventType.PrimaryHoverStay); setupCallback(ref OnPerControllerPrimaryHoverBegin, EventType.PerControllerPrimaryHoverBegin); setupCallback(ref OnPerControllerPrimaryHoverEnd, EventType.PerControllerPrimaryHoverEnd); setupCallback(ref OnGraspBegin, EventType.GraspBegin); setupCallback(ref OnGraspEnd, EventType.GraspEnd); setupCallback(ref OnGraspStay, EventType.GraspStay); setupCallback(ref OnPerControllerGraspBegin, EventType.PerControllerGraspBegin); setupCallback(ref OnPerControllerGraspEnd, EventType.PerControllerGraspEnd); setupCallback(ref OnSuspensionBegin, EventType.SuspensionBegin); setupCallback(ref OnSuspensionEnd, EventType.SuspensionEnd); setupCallback(ref OnContactBegin, EventType.ContactBegin); setupCallback(ref OnContactEnd, EventType.ContactEnd); setupCallback(ref OnContactStay, EventType.ContactStay); setupCallback(ref OnPerControllerContactBegin, EventType.PerControllerContactBegin); setupCallback(ref OnPerControllerContactEnd, EventType.PerControllerContactEnd); } private void setupCallback(ref Action action, EventType type) { if (_eventTable.HasUnityEvent((int)type)) { action += () => _eventTable.Invoke((int)type); } else { action += () => { }; } } private void setupCallback(ref Action action, EventType type) { if (_eventTable.HasUnityEvent((int)type)) { action += (h) => _eventTable.Invoke((int)type); } else { action += (h) => { }; } } #endregion } }